using System.Collections.Immutable;
using System.Text;

namespace Kook.Commands;

internal static class CommandParser
{
    private enum ParserPart
    {
        None,
        Parameter,
        QuotedParameter
    }

    public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs,
        IServiceProvider services, string input, int startPos, IReadOnlyDictionary<char, char> aliasMap)
    {
        ParameterInfo? curParam = null;
        StringBuilder argBuilder = new(input.Length);
        int endPos = input.Length;
        ParserPart curPart = ParserPart.None;
        int lastArgEndPos = int.MinValue;
        ImmutableArray<TypeReaderResult>.Builder argList = ImmutableArray.CreateBuilder<TypeReaderResult>();
        ImmutableArray<TypeReaderResult>.Builder paramList = ImmutableArray.CreateBuilder<TypeReaderResult>();
        bool isEscaping = false;
        char c, matchQuote = '\0';

        // local helper functions
        bool IsOpenQuote(IReadOnlyDictionary<char, char> dict, char ch)
        {
            // return if the key is contained in the dictionary if it is populated
            if (dict.Count != 0) return dict.ContainsKey(ch);

            // or otherwise if it is the default double quote
            return c == '\"';
        }

        static char GetMatch(IReadOnlyDictionary<char, char> dict, char ch)
        {
            // get the corresponding value for the key, if it exists
            // and if the dictionary is populated
            if (dict.Count != 0 && dict.TryGetValue(ch, out char value)) return value;

            // or get the default pair of the default double quote
            return '\"';
        }

        for (int curPos = startPos; curPos <= endPos; curPos++)
        {
            if (curPos < endPos)
                c = input[curPos];
            else
                c = '\0';

            //If we're processing an remainder parameter, ignore all other logic
            if (curParam != null && curParam.IsRemainder && curPos != endPos)
            {
                argBuilder.Append(c);
                continue;
            }

            //If this character is escaped, skip it
            if (isEscaping && curPos != endPos)
            {
                // if this character matches the quotation mark of the end of the string
                // means that it should be escaped
                // but if is not, then there is no reason to escape it then
                if (c != matchQuote)
                    // if no reason to escape the next character, then re-add \ to the arg
                    argBuilder.Append('\\');

                argBuilder.Append(c);
                isEscaping = false;
                continue;
            }

            //Are we escaping the next character?
            if (c == '\\' && (curParam == null || !curParam.IsRemainder))
            {
                isEscaping = true;
                continue;
            }

            //If we're not currently processing one, are we starting the next argument yet?
            if (curPart == ParserPart.None)
            {
                if (char.IsWhiteSpace(c) || curPos == endPos)
                    continue; //Skip whitespace between arguments
                else if (curPos == lastArgEndPos)
                    return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments.");
                else
                {
                    if (curParam == null) curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null;

                    if (curParam != null && curParam.IsRemainder)
                    {
                        argBuilder.Append(c);
                        continue;
                    }

                    if (IsOpenQuote(aliasMap, c))
                    {
                        curPart = ParserPart.QuotedParameter;
                        matchQuote = GetMatch(aliasMap, c);
                        continue;
                    }

                    curPart = ParserPart.Parameter;
                }
            }

            //Has this parameter ended yet?
            string? argString = null;
            if (curPart == ParserPart.Parameter)
            {
                if (curPos == endPos || char.IsWhiteSpace(c))
                {
                    argString = argBuilder.ToString();
                    lastArgEndPos = curPos;
                }
                else
                    argBuilder.Append(c);
            }
            else if (curPart == ParserPart.QuotedParameter)
            {
                if (c == matchQuote)
                {
                    argString = argBuilder.ToString(); //Remove quotes
                    lastArgEndPos = curPos + 1;
                }
                else
                    argBuilder.Append(c);
            }

            if (argString != null)
            {
                if (curParam == null)
                {
                    if (command.IgnoreExtraArgs)
                        break;
                    return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters.");
                }

                TypeReaderResult typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false);
                if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches)
                    return ParseResult.FromError(typeReaderResult, curParam);

                if (curParam.IsMultiple)
                {
                    paramList.Add(typeReaderResult);
                    curPart = ParserPart.None;
                }
                else
                {
                    argList.Add(typeReaderResult);
                    curParam = null;
                    curPart = ParserPart.None;
                }

                argBuilder.Clear();
            }
        }

        if (curParam is { IsRemainder: true })
        {
            TypeReaderResult typeReaderResult = await curParam
                .ParseAsync(context, argBuilder.ToString(), services)
                .ConfigureAwait(false);
            if (!typeReaderResult.IsSuccess)
                return ParseResult.FromError(typeReaderResult, curParam);

            argList.Add(typeReaderResult);
        }

        if (isEscaping)
            return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape.");
        if (curPart == ParserPart.QuotedParameter)
            return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete.");

        //Add missing optionals
        for (int i = argList.Count; i < command.Parameters.Count; i++)
        {
            ParameterInfo param = command.Parameters[i];
            if (param.IsMultiple)
                continue;
            if (!param.IsOptional)
                return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters.");
            argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue));
        }

        return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable());
    }
}
